﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/Validation.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

using Haplous.Properties;

using JetBrains.Annotations;

namespace Haplous.Correctness
{
    /// <summary>
    ///     An enumeration of the types of validation that must be applied to a string argument.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1714:FlagsEnumsShouldHavePluralNames")]
    [Flags]
    public enum StringMustNotBe
    {
        /// <summary>
        ///     The string argument must not be <see langword="null" />.
        /// </summary>
        Null = 1,

        /// <summary>
        ///     The string argument must not be empty.
        /// </summary>
        Empty = 2,

        /// <summary>
        ///     The string argument must not be whitespace.
        /// </summary>
        WhiteSpace = 4,

        /// <summary>
        ///     The string argument must not be <see langword="null" /> or empty.
        /// </summary>
        NullOrEmpty = Null | Empty,

        /// <summary>
        ///     The string argument must not be <see langword="null" /> or empty or whitespace
        /// </summary>
        NullOrEmptyOrWhiteSpace = NullOrEmpty | WhiteSpace
    }

    /// <summary>
    ///     An enumerable of the types of validation that must be applied to an enumerable.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1714:FlagsEnumsShouldHavePluralNames")]
    [Flags]
    public enum EnumMustNotBe
    {
        /// <summary>
        ///     The enumerable must not be null.
        /// </summary>
        Null = 1,

        /// <summary>
        ///     The enumerable must not be empty.
        /// </summary>
        Empty = 2,

        /// <summary>
        ///     The enumerable must not be null or empty.
        /// </summary>
        NullOrEmpty = Null | Empty
    }

    /// <summary>
    ///     Extension methods used to support the validation of arguments.
    /// </summary>
    internal static class Validation
    {
        /*
        * General notes about some of the attributes used in this class:
        * 
        *  - InvokerParameterNameAttribute This is a JetBrains attribute used to indicate that the parameter should contain
        *                                  the name of a parameter of the calling function.    
        *  - ValidatedNotNull              A code contracts attribute used to assert that the marked parameter is checked
        *                                  as not null by the function.  This allows Code Analysis to recognise the checked values.
        *  - AssertionCondition(AssertionConditionType.IsNotNull)
        *                                  A JetBrains attribute that performs much the same function as the ValidatedNotNull one.
        *  - ContractAbbreviator           A code contracts attribute used to mark methods that assert the validity of a callers 
        *                                  contract.
        *  - AssertionMethod               A JetBrains attribute that performs the same function as the ContractAbbreviator one.
        *  - NoEnumeration                 A JetBrains attribute that indicates that an enumerable argument is not enumerated in the
        *                                  attributed method to prevent the "Possible multiple enumeration of IEnumerable" warning.
        * 
        **/

        /// <summary>
        ///     Guarantees that the given argument is not <see langword="null" /> and returns it.
        /// </summary>
        /// <typeparam name="T">The type of the argument (must be a reference type).</typeparam>
        /// <param name="any">The value to be checked.</param>
        /// <returns>The value given in <paramref name="any" />.</returns>
        /// <exception cref="System.ArgumentNullException">
        ///     The <paramref name="any" /> argument was found to be
        ///     <see langword="null" />, it must not be <see langword="null" />.
        /// </exception>
        [NotNull]
        [DebuggerStepThrough]
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [UsedImplicitly]
        public static T NotNull<T>([CanBeNull] this T any) where T : class
        {
            if(any == null)
                throw new ArgumentNullException("any", String.Format(CultureInfo.InvariantCulture, "The 'any' argument was found to be null, it must not be null."));

            return any;
        }

        /// <summary>
        ///     Tests that the argument supplied is not <see langword="null" /> and throws an <see cref="ArgumentNullException" />
        ///     if it is.
        /// </summary>
        /// <typeparam name="T">The type of the argument (must be a <c>ref</c> type).</typeparam>
        /// <param name="argument">The argument to test.</param>
        /// <param name="argumentName">The name of the argument being tested.</param>
        /// <exception cref="System.ArgumentNullException">The argument must not be <see langword="null" />.</exception>
        [DebuggerStepThrough]
        [ContractAbbreviator]
        [AssertionMethod]
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [UsedImplicitly]
        public static void ValidateArgument<T>([ValidatedNotNull] [NoEnumeration] [CanBeNull] this T argument, [InvokerParameterName] [NotNull] string argumentName) where T : class
        {
            Debug.Assert(!String.IsNullOrWhiteSpace(argumentName), "'argumentName' must contain a valid argument name.");
            if(argument == null)
// ReSharper disable once AssignNullToNotNullAttribute
                throw new ArgumentNullException(argumentName, String.Format(CultureInfo.InvariantCulture, Resources.Error_PropertyMustNotBeNull, argumentName));
        }

        /// <summary>
        ///     Tests that the argument supplied is not <see langword="null" /> and throws an <see cref="ArgumentNullException" />
        ///     if it is.
        /// </summary>
        /// <typeparam name="T">The type of the argument (must be a <c>ref</c> type).</typeparam>
        /// <param name="argument">The argument to test.</param>
        /// <param name="argumentName">The name of the argument being tested.</param>
        /// <param name="validations">The types of validation to be applied.</param>
        /// <exception cref="System.ArgumentNullException">The argument must not be <see langword="null" />.</exception>
        [DebuggerStepThrough]
        [ContractAbbreviator]
        [AssertionMethod]
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [UsedImplicitly]
        public static void ValidateArgument<T>([ValidatedNotNull] [NoEnumeration] [CanBeNull] this IEnumerable<T> argument, [InvokerParameterName] [NotNull] string argumentName,
                                               EnumMustNotBe validations)
        {
            Debug.Assert(!String.IsNullOrWhiteSpace(argumentName), "'argumentName' must contain a valid argument name.");
            if((validations.HasFlag(EnumMustNotBe.Null) || validations.HasFlag(EnumMustNotBe.Empty)) && (argument == null))
// ReSharper disable once AssignNullToNotNullAttribute
                throw new ArgumentNullException(argumentName, String.Format(CultureInfo.InvariantCulture, Resources.Error_PropertyMustNotBeNull, argumentName));

            if(validations.HasFlag(EnumMustNotBe.Empty))
            {
                Debug.Assert(argument != null, "'argument' will never be null at this point.");

                var enumerator = argument.GetEnumerator();
                if(!enumerator.MoveNext())
// ReSharper disable once AssignNullToNotNullAttribute
                    throw new ArgumentOutOfRangeException(argumentName, String.Format(CultureInfo.InvariantCulture, Resources.Error_MustContainAtLeastOneItem, argumentName));
            }
        }

        /// <summary>
        ///     Validates the <see cref="string" /> argument given.
        /// </summary>
        /// <param name="argument">The argument to test.</param>
        /// <param name="argumentName">The name of the argument being tested.</param>
        /// <param name="validations">The types of validation to perform.</param>
        /// <param name="additionalInformation">The additional information to append to any error message.</param>
        /// <exception cref="System.ArgumentNullException">The argument must not be <see langword="null" />.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">The argument must not be empty.</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">The argument must not contain only whitespace.</exception>
        [DebuggerStepThrough]
        [ContractAbbreviator]
        [AssertionMethod]
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [UsedImplicitly]
        public static void ValidateArgument([ValidatedNotNull] [CanBeNull] this string argument, [InvokerParameterName] [NotNull] string argumentName, StringMustNotBe validations,
                                            [CanBeNull] string additionalInformation = null)
        {
            Debug.Assert(!String.IsNullOrWhiteSpace(argumentName), "'argumentName' must contain a valid argument name.");

            var isNull = (argument == null);

            if(validations.HasFlag(StringMustNotBe.Null) && isNull)
                throw new ArgumentNullException(argumentName, String.Format(CultureInfo.InvariantCulture, Resources.Error_PropertyMustNotBeNull + (additionalInformation ?? String.Empty), argumentName));
            if(validations.HasFlag(StringMustNotBe.Empty) && !isNull && (argument.Length <= 0))
            {
                throw new ArgumentOutOfRangeException(argumentName, argument,
                                                      String.Format(CultureInfo.InvariantCulture, "'{0}' must not be an empty string." + (additionalInformation ?? String.Empty), argumentName));
            }
            if(validations.HasFlag(StringMustNotBe.WhiteSpace) && !isNull && (argument.Trim().Length <= 0))
            {
                throw new ArgumentOutOfRangeException(argumentName, argument,
                                                      String.Format(CultureInfo.InvariantCulture, "'{0}' must not contain only whitespace." + (additionalInformation ?? String.Empty), argumentName));
            }
        }

        /// <summary>
        ///     Validates the property assignment.
        /// </summary>
        /// <param name="value">The value being assigned.</param>
        /// <param name="propertyName">The name of the property.</param>
        /// <param name="canBeNull">
        ///     If set to <see langword="false" /> and <paramref name="value" /> is <see langword="null" /> and
        ///     exception will be thrown.
        /// </param>
        /// <param name="minLength">
        ///     The minimum permitted length.  Set to 0 to prevent empty strings. Set to -1 if there is no
        ///     minimum length
        /// </param>
        /// <param name="maxLength">The maximum permitted length.  Set to -1 if there is no maximum length.</param>
        /// <exception cref="System.ArgumentNullException">
        ///     Thrown if <paramref name="canBeNull" /> is <see langword="false" /> and
        ///     <paramref name="value" /> is <see langword="null" />.
        /// </exception>
        /// <exception cref="System.ArgumentOutOfRangeException">
        ///     Thrown if <paramref name="value" /> is longer than the maximum
        ///     length specified in <paramref name="maxLength" />
        /// </exception>
        /// <exception cref="System.ArgumentOutOfRangeException">
        ///     Thrown if <paramref name="value" /> is shorter than the minimum
        ///     length specified in <paramref name="minLength" />
        /// </exception>
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [UsedImplicitly]
        public static void ValidateProperty([CanBeNull] this string value, [NotNull] string propertyName, bool canBeNull, int minLength, int maxLength)
        {
            if(!canBeNull && (value == null))
// ReSharper disable once AssignNullToNotNullAttribute
                throw new ArgumentNullException(propertyName, String.Format(CultureInfo.InvariantCulture, Resources.Error_PropertyMustNotBeNull, propertyName));

            if((minLength >= 0) && (value != null) && (value.Length < minLength))
// ReSharper disable once AssignNullToNotNullAttribute
                throw new ArgumentOutOfRangeException(propertyName, String.Format(CultureInfo.InvariantCulture, Resources.Error_PropertyTooShort, propertyName, minLength));

            if((maxLength >= 0) && (value != null) && (value.Length > maxLength))
// ReSharper disable once AssignNullToNotNullAttribute
                throw new ArgumentOutOfRangeException(propertyName, String.Format(CultureInfo.InvariantCulture, Resources.Error_PropertyTooLong, propertyName, maxLength));
        }

        /// <summary>
        ///     Validates the enum argument given is a member of the enum.
        /// </summary>
        /// <param name="argument">The argument to be validated.</param>
        /// <param name="argumentName">The name of the argument to use if an exception must be thrown.</param>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        [UsedImplicitly]
        public static void ValidateEnumArgument([NotNull] this Enum argument, [InvokerParameterName] [NotNull] string argumentName)
        {
            Debug.Assert(!String.IsNullOrWhiteSpace(argumentName), "'argumentName' must contain a valid argument name.");

            if(Enum.IsDefined(argument.GetType(), argument))
                return;

            throw new ArgumentOutOfRangeException(argumentName, argument, // ReSharper disable once AssignNullToNotNullAttribute
                                                  string.Format(CultureInfo.InvariantCulture, Resources.Error_InvalidEnumeration, argumentName, argument.GetType().Name, argument));
        }
    }
}